#!/usr/bin/env perl
# Copyright 2010-2011 Ricky Thomson - <punkrockunix@gmail.com>
#
# RuneMage is free software; you can redistribute it and/or modify 
# it under the terms of the GNU General Public License as published 
# by the Free Software Foundation; either version 2 of the License, 
# or (at your option) any later version.
#
# RuneMage is distributed in the hope that it will be useful, but 
# WITHOUT ANY WARRANTY; without even the implied warranty of 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License 
# along with RuneMage; if not, write to the Free Software Foundation, 
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

use strict;
use warnings;
use utf8;

use threads;

use FindBin qw($Bin); 
use Glib qw{ TRUE FALSE };
use Gtk2 '-init';
use Gtk2::MozEmbed;
use XML::Simple;
use LWP::Simple qw($ua getstore get);

# the amount of seconds we should for GET timeouts
$ua->timeout(4);

#----------------------------------------------------------
#
#					make sure we are not root! 
#
#----------------------------------------------------------

if ( $< == 0 ) { 
	print "\nWARNING: Running this program as 'root' is stupid!\nAre you absolutely sure you want to continue?\n\n[yes/no]: ";
	
	if (my $prompt = <STDIN> =~ /^yes$/i) {
    	goto init;
	} else {
     	exit 0;
	}
}

# breaks the if statement when we chose yes.
init:



#----------------------------------------------------------
#
#					check arguments 
#
#----------------------------------------------------------

my $verbose = 0;
	
if ( $#ARGV >= 0 ) {
	if ( $ARGV[0] eq "--version" ) {
		# display licence / version info
		license();
		exit 0;
	}
	
	if ( $ARGV[0] eq "--debug" ) {
		# switch on verbose output
		$verbose = 1;
	}
}




#----------------------------------------------------------
#
# 					set some globals
#
#----------------------------------------------------------



my $version = "0.7.0"; 
	
if ( ! $verbose == 0 ) {
	print "RuneMage v$version - Copyright 2010-2011; Ricky T <punkrockunix\@gmail.com>    \n\n";
}
	
### location to store profile/settings
my $home = $ENV{ HOME } . "/.runemage/";
	
if ( ! $verbose == 0 ) {
	print "** CONF - Using '$home' to store local files.\n";
}
	
### mozilla cache/profile data path
### (goes inside local data /home/user/.runemage/ )
	Gtk2::MozEmbed -> set_profile_path( $home, "mozilla_cache" );

### folder where images will be saved/cached
### (goes inside local data /home/user/.runemage/ )
my $icon_store	  = $home . "skill_icons/";
my $ge_store	  = $home . "ge_cache/";

### url/filepath defaults
my $game 	= "http://runescape.com/";
my $classic	= "http://runescape.com/classicapplet/playclassic.ws";
my $hiscore	= "http://hiscore.runescape.com/index_lite.ws?player=";
my $gebase	= "http://services.runescape.com/m=itemdb_rs";
my $splash 	= $Bin . "/data/start.html";
my $gladexml	= $Bin . "/data/runemage.xml";

### base url of the skill icons we need
my $icon_location = "http://www.runescape.com/img/hiscores/";


### skill icon filenames
my @icons = ("skill_icon_attack1.gif", 	"skill_icon_defence1.gif",
			"skill_icon_strength1.gif", 	"skill_icon_hitpoints1.gif",
			"skill_icon_ranged1.gif", 	"skill_icon_prayer1.gif",
			"skill_icon_magic1.gif", 	"skill_icon_cooking1.gif",
			"skill_icon_woodcutting1.gif", 	"skill_icon_fletching1.gif",
			"skill_icon_fishing1.gif", 	"skill_icon_firemaking1.gif",
			"skill_icon_crafting1.gif", 	"skill_icon_smithing1.gif", 
			"skill_icon_mining1.gif", 	"skill_icon_herblore1.gif",
			"skill_icon_agility1.gif", 	"skill_icon_thieving1.gif",
			"skill_icon_slayer1.gif", 	"skill_icon_farming1.gif",
			"skill_icon_runecraft1.gif", 	"skill_icon_hunter1.gif",
			"skill_icon_construction1.gif", "skill_icon_summoning1.gif", 
			"skill_icon_dungeoneering1.gif", #"star_members.png", "star_free.png"
			#TODO: move .png filenames somewhere else.
);


my ( $builder, $window, $moz );
use subs qw{ main on_window_destroy };


#----------------------------------------------------------
#
# 					startup checks
#
#----------------------------------------------------------

# Check for a splash page. (doesnt need to exist)

if ( -e $splash ) { 
	if ( ! $verbose == 0 ) {
		print "** DATA - $splash\n";
	}

} else { 
	warn "** '$splash' $!"; 
}


# Make sure gladexml (interface) file exists or die. (we need this!)

if ( -e $gladexml ) {  
	if ( ! $verbose == 0 ) {
		print "** DATA - $gladexml\n";
	}
} else { 
	die "** '$gladexml' $!"; 
}

# Make needed directories for config / cache stores
# ... download skill icons if not present.

if ( ! -e $ge_store ) { 
	mkdir $ge_store or die $!; 
}

if ( ! -e $icon_store ) { 
	mkdir $icon_store or die $!; 
}


#----------------------------------------------------------
#
# 	loop through skill icon filenames and download them
#
#----------------------------------------------------------

my @icon_threads;

foreach ( @icons ) {
	if ( ! -e "$icon_store$_" ) {
			

		push @icon_threads, threads->new( 
			sub { 
				getstore( "$icon_location$_" , "$icon_store$_" );
					if ( ! $verbose == 0 ) { print "** HTTP - Fetching '$_'\n"; }

			}
		);
	} else {
		if ( ! $verbose == 0 ) { print "** DATA - Found '$icon_store$_'\n"; }
	}
}

# rejoin threads 
foreach (@icon_threads) {
	$_->join();
}

@icon_threads = ();


#----------------------------------------------------------
#
# 					start the program
#
#----------------------------------------------------------


&main;


#----------------------------------------------------------
#
# 					subroutines/callbacks
#
#----------------------------------------------------------

sub main {


	$builder = Gtk2::Builder->new();

	# create a mozembed widget
	$moz = Gtk2::MozEmbed -> new();
	
	# load converted glade XML file
	$builder->add_from_file( $gladexml );


	# get top level object as named in glade XML
	$window = $builder->get_object( 'window' );



	# resize the window
	$window->set_default_size(1010, 620);

	$builder->connect_signals( undef );



	# define object that should hold browser widget
	our $gamewindow = $builder->get_object( 'browser_frame' );


	# load offline welcome/splash page
	destination_url( $splash );

	# because cache/icons now live in ~/.runemage/*
	# we need to manually set the image by doing this:
	foreach ( @icons ) { $builder->get_object( $_ )->set_from_file( "$icon_store$_" ); }


		# pack mozilla widget
		$gamewindow -> add( $moz );

		# draw the window
		$window->show_all();

	Gtk2->main();

}


sub gtk_main_quit {
	
	# clean shutdown
	$window->destroy;

	Gtk2->main_quit();

}

# add commas to string of numbers, 1234567 returns 1,234,567
sub commify {
	local $_ = shift;
	1 while s/^([-+]?\d+)(\d{3})/$1,$2/;
	return $_;
}

# trims whitespace, leaving single space intact
sub trim {
	my $string = shift;
	$string =~ s/^\s+//;
	$string =~ s/\s+$//;
	return $string;
}



# handles the webpage to be loaded
sub destination_url { $moz -> load_url( @_ ); }


# fetch a text entry string
sub grab_text_entry{ $builder->get_object( @_ )->get_text; }

# set a text entry
sub set_text_entry { our ( $label, $text ) = @_; $builder->get_object( $label )->set_text( $text ); }




#----------------------------------------------------------
#
# 					hiscore utility
#
#----------------------------------------------------------



sub on_btnSearch_clicked {


	# get the text input string of "search_entry"
	our $rsn = grab_text_entry('hiscore_search');
		

	# fetch data from hiscore of text input
	our $data = get( $hiscore . $rsn ); 


	# if user doesn't exist / not ranked at all (no data is returned, 404)
	# in that case we can quit the whole sub after showing an error message
		if ( !$data ) {

			# This should be displayed as an alert box (probably annoying though)

			&clear_hiscores();
			
			return;
		}

	# otherwise continue, replace newline characters for commas
		$data =~ s/[\n]/,/g; 


	# split string into array at each comma
		our @s = ( split(/,/, $data ) );


	# pass appropriate values to skill labels and set them
			  #LABEL TO USE#	#SKILL#			#LVL#	#RANK#	 #EXP#
		set_skill("lbl_attack"	    , "Attack" 		,	$s[4]  , $s[3]  ,$s[5]	);
		set_skill("lbl_defence"     , "Defence"		,	$s[7]  , $s[6]  ,$s[8]	);
		set_skill("lbl_strength"    , "Strength"	,	$s[10] , $s[9]  ,$s[11]	);
		set_skill("lbl_hitpoints"   , "Constitution"	,	$s[13] , $s[12] ,$s[14]	);
		set_skill("lbl_ranged"      , "Ranged"		,	$s[16] , $s[15] ,$s[17]	);
		set_skill("lbl_prayer"      , "Prayer"		,	$s[19] , $s[18] ,$s[20]	);
		set_skill("lbl_magic"       , "Magic"		,	$s[22] , $s[21] ,$s[23]	);
		set_skill("lbl_cooking"     , "Cooking"		,	$s[25] , $s[24] ,$s[26]	);
		set_skill("lbl_wc"          , "Woodcutting"	,	$s[28] , $s[27] ,$s[29]	);
		set_skill("lbl_fletch"      , "Fletching"	,	$s[31] , $s[30] ,$s[32]	);
		set_skill("lbl_fishing"     , "Fishing"		,	$s[34] , $s[33] ,$s[35]	);
		set_skill("lbl_fm"          , "Firemaking"	,	$s[37] , $s[36] ,$s[38] );
		set_skill("lbl_craft"       , "Crafting"	,	$s[40] , $s[39] ,$s[41]	);
		set_skill("lbl_smithing"    , "Smithing"	,	$s[43] , $s[42] ,$s[44]	);
		set_skill("lbl_mining"      , "Mining"		,	$s[46] , $s[45] ,$s[47]	);
		set_skill("lbl_herblore"    , "Herblore"	,	$s[49] , $s[48] ,$s[50]	);
		set_skill("lbl_agility"     , "Agility"		,	$s[52] , $s[51] ,$s[53]	);
		set_skill("lbl_thieveing"   , "Thieving"	,	$s[55] , $s[54] ,$s[56] );
		set_skill("lbl_slayer"      , "Slayer"		,	$s[58] , $s[57] ,$s[59]	);
		set_skill("lbl_farm"        , "Farming"		,	$s[61] , $s[60] ,$s[62]	);
		set_skill("lbl_rc"          , "Runecrafting"	,	$s[64] , $s[63] ,$s[65]	);
		set_skill("lbl_hunter"      , "Hunter"		,	$s[67] , $s[66] ,$s[68]	);	
		set_skill("lbl_construction", "Construction"	,	$s[70] , $s[69] ,$s[71] );
		set_skill("lbl_summon"      , "Summoning"	,	$s[73] , $s[72] ,$s[74]	);
		set_skill("lbl_dungeon"     , "Dungeoneering"	,	$s[76] , $s[75] ,$s[77]	);

		# don't want tooltips here (for the moment) on total values
		# passing a 4th argument will create a tooltip

		set_skill("lbl_rank"	    , "Overall Rank"	,	$s[0] );
		set_skill("lbl_total"       , "Total Level"	,	$s[1] );
		set_skill("lbl_xp"          , "Total XP"	,	$s[2] );

}


sub set_skill {

	# pass name of label to display value parsed from hiscores lookup
	our ( $label, $skill,  $lvl, $rank, $exp ) = @_;


	# clean up output for unranked skills
	# -1 is used for unranked in returned data, we display 0 instead. 
	if ( $lvl == -1 ) { 
		$rank = "n/a";
		$exp  = "n/a";
		$lvl  = "0";
	} else {
		# add commas to value (level will never exceed 3 digits,
		# however "totals" share this value).
		$lvl  = commify( "$lvl"	);

	}

	# update skill label
	set_text_entry( $label, $lvl );


	# only if rank and exp was passed to us... (to avoid errors when setting "totals")
	if ( $rank ) {
		# add commas, to ranks and exp values
		$rank = commify( "$rank" );
		$exp  = commify( "$exp"  );

		# update tooltip data
		our $tooltip = "$skill\nRank: $rank\nExp: $exp";
		$builder->get_object( $label )->set_tooltip_text( $tooltip );
	}

}

sub clear_hiscores {
		# this could be done a different way.....
		
		set_text_entry("hiscore_search"	 , "Error!" 	);
				
		set_text_entry("lbl_attack"	 , "0" 	);
		set_text_entry("lbl_defence"     , "0"	);
		set_text_entry("lbl_strength"    , "0"	);
		set_text_entry("lbl_hitpoints"   , "0"	);
		set_text_entry("lbl_ranged"      , "0"	);
		set_text_entry("lbl_prayer"      , "0"	);
		set_text_entry("lbl_magic"       , "0"	);
		set_text_entry("lbl_cooking"     , "0"	);
		set_text_entry("lbl_wc"          , "0"	);
		set_text_entry("lbl_fletch"      , "0"	);
		set_text_entry("lbl_fishing"     , "0"	);
		set_text_entry("lbl_fm"          , "0"	);
		set_text_entry("lbl_craft"       , "0"	);
		set_text_entry("lbl_smithing"    , "0"	);
		set_text_entry("lbl_mining"      , "0"	);
		set_text_entry("lbl_herblore"    , "0"	);
		set_text_entry("lbl_agility"     , "0"	);
		set_text_entry("lbl_thieveing"   , "0"  );
		set_text_entry("lbl_slayer"      , "0"	);
		set_text_entry("lbl_farm"        , "0"	);
		set_text_entry("lbl_rc"          , "0"	);
		set_text_entry("lbl_hunter"      , "0"	);	
		set_text_entry("lbl_construction", "0"  );
		set_text_entry("lbl_summon"      , "0"	);
		set_text_entry("lbl_dungeon"     , "0"	);

		set_text_entry("lbl_rank"	 , "0"  );
		set_text_entry("lbl_total"       , "0"  );
		set_text_entry("lbl_xp"          , "0"  );
}


#----------------------------------------------------------
#
# 					web page selection
#
#----------------------------------------------------------

sub on_File_Play_Runescape_activate 			{ destination_url( $game    ); }
sub on_File_Play_Runescape_Classic_activate   	{ destination_url( $classic ); }
sub on_File_Home_activate 	   					{ destination_url( $splash  ); }
#TODO: add text file of fansites (optional) which will be read in here,
# and listed under "Links" on the toolbar.

#----------------------------------------------------------
#
# 					about dialog
#
#----------------------------------------------------------

sub on_about_clicked {
	our $about = $builder->get_object( 'aboutdialog' );

	# displays the about dialog
	$about->run;

	# make sure it goes away when closed
	$about->hide;
}


#----------------------------------------------------------
#
# 					window options
#
#----------------------------------------------------------

sub on_toggleview_sidepane_activate {
	our $sp = $builder->get_object('vbox_sidepane');

	# toggle display of sidepane
	if ($sp->visible == 1) {
		$sp->hide_all(); 
	} else { 
		$sp->show_all(); 
	}
	
}

#TODO: unrefferenced at the moment.
sub on_toggle_sidepane_position_activate {

	our $hbox = $builder->get_object('hbox_main');
	our $sp = $builder->get_object('vbox_sidepane');

		#$hbox->reorder_child( $sp, 0 );  #right
		#$hbox->reorder_child( $sp, 1 );  #left

}


### toggles display of other "expander" sections when selecting
### a different one.
sub on_hs_expander_activate { close_expander( 'ge', 'al' ); }
sub on_ge_expander_activate { close_expander( 'al', 'hs' ); }
sub on_al_expander_activate { close_expander( 'hs', 'ge' ); }


sub close_expander {
	# close all but selected expander panes
	foreach our $section ( @_ ) {
		$builder->get_object( $section . "_expander" )->set_expanded(0);
		
	}
}


#----------------------------------------------------------
#
# 					grand exchange utility
#
#----------------------------------------------------------
#TODO: rewrite this, its a frikkin mess.

sub on_btnPrice_clicked {	
	
	our $item = grab_text_entry( 'price_entry' );

	if  ( $item eq "" ) { 
		clearGe();
		return; 
	} 

	print "** Fetching item information for \"$item\"...\n";

	our ( $itemUrl, $imageSmall, $imageBig, $objcode, $itemId, $itemName, $itemFileName );

	# fetch (huge page) so we can parse ge values of the item
	our $content = get ( $gebase . '/results.ws?query=' . $item ) || clearGe() && return;

	# grab links for images, the object "code", and the item id
	# NOTE: the object "code" is a unique part of the url )tends to change once in a while)
	#       and is required to get more information such as an image.
	if ( $content =~ m/<td><img src="$gebase\/(\d+)_obj_sprite.gif\?id=(\d+)" alt="(.+)"><\/td>/g ) {

		( $objcode, $itemId, $itemName ) = ( $1, $2, $3 );
		
		$itemFileName = $itemName;
		$imageSmall =  $gebase . '/' . $objcode . '_obj_sprite.gif?id=' . $itemId ;
		$imageBig   =  $gebase . '/' . $objcode . '_obj_big.gif?id=' . $itemId;
		$itemUrl    =  $gebase . '/' .  $itemName . '/viewitem.ws?obj=' . $itemId;

	}
		


	#######	 Cache Icons (sprites of item) #########
	# store image/sprite to cache folder (if it doesn't exist)
	if ( ! -e "$ge_store/$itemName" . '_sprite.gif' ) { 
	
		# check if the item name contains forward slashes (invalid filenames)
		if ( $itemName =~ m/[\/]/ ) { 

			# replace it for an underscore if it exists.
		 	$itemFileName =~ s/[\/]/_/g; 
		 	
		 	# fetch the image file
			getstore( $imageSmall, "$ge_store/$itemFileName" . '_sprite.gif' ) ;
			
		} else {
			
			# fetch the image file
			getstore( $imageSmall, "$ge_store/$itemFileName" . '_sprite.gif' ) ;
			
		}
	}

	#######	 Cache Icons (images of item) #########
	# store image/sprite to cache folder (if it doesn't exist)
	if ( ! -e "$ge_store/$itemName" . '.gif' ) { 
	
		# check if the item name contains forward slashes (invalid filenames)
		if ( $itemName =~ m/[\/]/ ) { 

			# replace it for an underscore if it exists.
		 	$itemFileName =~ s/[\/]/_/g; 
		 	
		 	# fetch the image file
			getstore( $imageBig, "$ge_store/$itemFileName" . '.gif' ) ;
			
		} else {
		
			# fetch the image file
			getstore( $imageBig, "$ge_store/$itemFileName" . '.gif' ) ;
			
		}
	}

	# replace space for newline (stop text overflow on interface)
	$itemName =~ s/\ /\n/g;

	set_text_entry( 'lbl_item_str', $itemName );	

	######## today price change #########
	if ( $content =~ m/<span class="(drop|stay|rise)">(.+)<\/span>/g ) { 
		set_text_entry( 'lbl_change_str', "Change: ". $2 . " (gp)" );	
	}

	######## free/members item ##########
	if ( $content =~ m/<img src="(.+)" alt="(Free game item|Members' item)"(.+)>/ ) {
		set_text_entry( 'lbl_members_str', $2 );	



		# decide whether the item is a "members only" item or a "free game" item.
		# and set the image acordingly.
		if ( $2 eq "Free game item" ) {
			$builder->get_object( 'free_or_members' )->set_from_file( "$ge_store/free.png" );
		} else {
			$builder->get_object( 'free_or_members' )->set_from_file( "$ge_store/members.png" );
		}

	}

	###### get more info on the item #######
	# get more info, after finding out the item's unique ID, we can then grab the data
	# from the item's actual page.

	our $itemInfo = get ( $itemUrl ) || clearGe() && return; 

	###### current guide price ##########
	if ( $itemInfo =~ m/<b>Current guide price:<\/b> (.+)/g ) { 
		set_text_entry( 'lbl_guideprice_str', "Current: " . $1 ." (gp)" );	
	}
	
	###### 30/90/180 day change ########
	if ( $itemInfo =~ m/<b>30 Days:<\/b> <span class="(drop|stay|rise)">(.+)<\/span>/g ) { 
		set_text_entry( 'lbl_30_str', "30 Days: " . $2 );
	}
	if ( $itemInfo =~ m/<b>90 Days:<\/b> <span class="(drop|stay|rise)">(.+)<\/span>/g ) { 
		set_text_entry( 'lbl_90_str', "90 Days: " . $2 );
	}
	if ( $itemInfo =~ m/<b>180 Days:<\/b> <span class="(drop|stay|rise)">(.+)<\/span>/g ) { 
		set_text_entry( 'lbl_180_str', "180 Days: " . $2 );
	}	

	# set large image	
	$builder->get_object( 'ge_large_img' )->set_from_file( "$ge_store$itemFileName" . ".gif" );

	# set small sprite
	# 'ge_item_img' does not exist any more... for now.
	#$builder->get_object( 'ge_item_img' )->set_from_file( "$ge_store$itemName" . "_sprite.gif" );

}

sub clearGe {
	# this could be done neater....
	print "** ERROR - There was an error fetching grand exchange data!\n\n";
	set_text_entry( 'lbl_item_str', "Error!" );
	set_text_entry( 'lbl_change_str', "n/a" );
	set_text_entry( 'lbl_guideprice_str', "n/a" );
	set_text_entry( 'lbl_30_str', "n/a" );
	set_text_entry( 'lbl_90_str', "n/a" );
	set_text_entry( 'lbl_180_str', "n/a" );
	$builder->get_object( 'ge_item_img' )->set_from_file( undef );
}



#----------------------------------------------------------
#
# 					adventure log utility
#
#----------------------------------------------------------



#TODO: seems to be more whitespace not being removed (especially with dungeoneering boss kills)
#TODO: error checking.
#TODO: add some more icons to be displayed next to the adventure logs type.
#      ex; show an icon that represents 'loot' if the activity is an item drop. 
sub on_btnAlog_clicked {

	# fetch recent activity from the adventurer's log rss feed

	our $rsn = grab_text_entry('alog_search');
	our $url = 'http://services.runescape.com/m=adventurers-log/rssfeed?searchName=';
	our $rss = get ( $url . $rsn )
			or warn $! && return;


	# loop through text labels 'alog_lbl0' <--> 'alog_lbl9'
	# and set label/tooltip data

        our ( $xml, $i ) = ( XMLin( $rss ), 0 );
        for ( $i=0; $i<10; $i++ )
        {

		# label we are going to set
		our $lbl = $builder->get_object( "alog_lbl" . $i );


		# the labels primary content
		our $lbltext =  ( $i +1 ) . ". $xml->{channel}->{item}->[$i]->{title}";


		# append "..." and cut string if exceeds 25 chars to stop text overflowing
		# and then set the tex to the label

		$lbltext =~ s/^(.{25}).+/$1.../;
	        $lbl->set_text( $lbltext );


		# the rss date {pubDate}
		our $rssDate = $xml->{channel}->{item}->[$i]->{pubDate};

		# trim() removes the horrible whitespace from {description}
		our $rssDesc = trim( $xml->{channel}->{item}->[$i]->{description} );


		# remove the time, always 00:00:00... pointless to show
		if ( $rssDate =~ m/(.+)\ (\d+):(\d+):(\d+)\ (.+)/ ) { $rssDate = $1; }

		# set the tooltip
		$lbl->set_tooltip_text( $rssDate . "\n" . $rssDesc );
	

        }

	# hidden by default
	#TODO: maybe remove this, or place it somewhere else
	our $aloglink = $builder->get_object('alog_link');
	$aloglink->show();

	# "more activity" set the link it will open
	$aloglink->set_uri( $url . $rsn );
}



#----------------------------------------------------------
#
# 					misc
#
#----------------------------------------------------------

sub license {
	print "RuneMage is free software; you can redistribute it and/or modify it  \n";
	print "under the terms of the GNU General Public License as published   \n";
	print "by the Free Software Foundation; either version 2 of the License,\n";
	print "or (at your option) any later version.                           \n\n";
	print "See the included file 'COPYING' for more information\n\n";
}

###EOF###



